#ifndef __M_SESSION_H__
#define __M_SESSION_H__

#include "online.hpp"

typedef enum {login, unlogin} ss_state;
class session
{
private:
    int _ssid;//会话标识符
    int _uid;//哪个用户的标识符
    ss_state _state;//session对应的用户的状态(未登录，已登录)
    wsserver_t::timer_ptr _tp;//session所关联的定时器对象
public:
    session(int ssid)
        :_ssid(ssid)
    { 
        DLOG("session %p 创建成功!", this); 
    }
    ~session()
    { 
        DLOG("session %p 销毁成功!", this); 
    }
    void set_user(int uid)//关联用户和session
    {
        _uid = uid;
    }
    int get_uid()//获取session关联的用户id
    {
        return _uid;
    }
    bool is_login()//判断用户是否处于登录状态
    {
        return _state == login;
    }
    void set_timer(const wsserver_t::timer_ptr& tp)//设置定时器对象
    {
        _tp = tp;
    }
    wsserver_t::timer_ptr& get_timer()//获取定时器对象
    {
        return _tp;
    }
    void set_state(ss_state state)
    {
        _state = state;
    }
    int ssid()
    {
        return _ssid;
    }
};

#define SESSION_TIMEOUT 15000
#define SESSION_PERMANENT -1

using session_ptr = std::shared_ptr<session>;

class session_manager
{
private:
    int _next_ssid;//session id分配器
    std::mutex _mutex;//保护session id分配器
    std::unordered_map<int, session_ptr> _sessions;//管理多个session的数据结构
    wsserver_t* _svr;//websocketpp::server类型的对象，用于获取set_timer()API，设置定时任务
public:
    session_manager(wsserver_t* svr)
        :_svr(svr), _next_ssid(1)
    {
        DLOG("session管理模块初始化完毕!");
    }
    ~session_manager()
    {
        DLOG("session管理模块销毁完毕!");
    }
    //我们的项目里面，没有不登陆就可以进行的操作，所以只要能够创建会话，则用户一定处于登录状态
    //所以创建会话的时候，我们不需要给予用户的登录状态这样的参数
    session_ptr create_session(int uid, ss_state state)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        session_ptr sp(new session(_next_ssid));//局部开辟在栈上的智能指针
        sp->set_state(state);//设置session的状态
        sp->set_user(uid);//设置session对应的uid
        _sessions.insert(std::make_pair(_next_ssid, sp));//开辟在堆上的智能指针
        _next_ssid++;
        return sp;
    }
    session_ptr is_session_exists(int uid)
    {
        for(auto& pair : _sessions)
        {
           session_ptr sp = pair.second;
           if(sp->get_uid() == uid) return sp;
        }
        return session_ptr();
    }
    session_ptr get_session_by_ssid(int ssid)//通过会话id，找到会话的详细信息
    {
        std::unique_lock<std::mutex> lock(_mutex);//涉及到对共享资源_sessions的访问
        auto it = _sessions.find(ssid);
        if(it == _sessions.end()) return session_ptr();
        return it->second;
    }
    void destroy_session(int ssid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _sessions.erase(ssid);
    }
    void set_session_expire_time(int ssid, int ms)//给某一个会话添加sec的过期时间
    {
        //依赖于websocketpp的定时器来完成session生命周期的管理。(session的三个状态: 刚创建session定时删除->永久存在->定时删除)
        //1.在session刚创建出来(用户刚登录成功)，此时若长时间无通信，则session就应该被销毁掉
        //  (登录成功后有一个消息弹窗，点击消息弹窗中的确定按钮后，页面会跳转至游戏大厅，此时客户端会向服务器申请建立websocket长连接
        //  如果我们长时间不点击确定按钮，那么session就应该在一段时间后被删除，用户此时就需要重新登录，服务器重新为用户建立会话)
        //2.在session设置了定时删除任务的情况下，将session设置为永久存在
        //  (当页面跳转到游戏大厅后，连接此时就要切换为websocket长连接，那么后续的session就必须一直永久存在，因为服务器处理任何一个请求
        //   业务之前，都会判断用户此时是否处于登录状态)
        //3.在session设置了定时删除任务的情况下，刷新session的过期时间
        //  (当用户进入游戏房间，下完棋分出胜负后，用户退出房间后，不想玩这个游戏了，直接退出游戏，那么此时session就要从永久变为指定时间段
        //   后删除的状态，为什么不能直接删除呢？万一用户又想回头跳转到游戏，看一下自己的天梯分数是多少呢？为了方便点，那我们别一下子就删除
        //   会话，让会话在保持一段时间，如果用户真的就退出了，没有任何操作了，那么待会儿session自己就会被删除了)

        session_ptr sp = get_session_by_ssid(ssid);// ---暂时获取保存一下session会话句柄---
        if(sp.get() == nullptr) return;//session不存在

        wsserver_t::timer_ptr tp = sp->get_timer();//获取定时器对象
        if(tp.get() == nullptr && ms == SESSION_PERMANENT)
        {
            //tp.get() == nullptr，我们就认为会话原来是永久存在的，现在又要让我们设置会话永久存在，
            // 那啥都不干就行了，什么定时任务都不要给会话设置，这样会话本身就依旧是永久存在的
            return;
        } 
        else if(tp.get() == nullptr && ms == SESSION_TIMEOUT)//从永久存在，设置为定时删除
        {
            //1.设置定时任务，在ms之后，会执行destroy_session()，将会话删除
            //---bind影响的是实际的传参，并不影响类型匹配。set_timer里面该怎么调怎么传参就怎么搞，只不过传参没啥用，因为bind已经绑死参数了---
            //---bind一般有两种用法，一种绑死，一种预留参数位置，等待函数内部传参---
            wsserver_t::timer_ptr tmp = _svr->set_timer(ms, std::bind(&session_manager::destroy_session, this, ssid));
            //2.将设置定时任务set_timer返回的定时器句柄，设置到会话所关联的定时器对象中
            sp->set_timer(tmp);//设置进去之后，下次tp.get()判断定时器是否存在的时候，才能判断session是永久还是定时删除的状态
        }
        else if(tp.get() != nullptr && ms == SESSION_PERMANENT)//从定时删除设置为永久存在
        {
            //1.取消定时任务，会导致任务直接被执行，也就是直接删除掉了会话句柄: session_ptr
            // bug->调用cancel后，定时任务并不是立即取消的，可能会出现，我们先执行了第二步之后，内核才去执行了第一步，那么此时会话就消失了
            tp->cancel();
            //2.所以我们要重新向哈希表中添加 (会话句柄和会话id) 的键值对
            // bug->所以这个插入的操作不能被立即执行，因为有可能出现先添加，然后被删除掉的情况！   
            // 解决方案: 把这条语句的执行设置为定时任务，而不是立即添加
            // _sessions.insert(std::make_pair(ssid, sp));// ---把暂时保存的会话句柄重新放到哈希表里面---
            _svr->set_timer(0, std::bind(&session_manager::append_already_session, this, sp, ssid));
            //3.通过会话句柄sp，向会话中设置一个空的定时器，代表此会话永久存在
            sp->set_timer(wsserver_t::timer_ptr());
        }
        else if(tp.get() != nullptr && ms == SESSION_TIMEOUT)//刷新重置定时删除的时间
        {
            //1.1 取消原来的定时任务，也就是调用destroy_session删除哈希表中的会话句柄
            tp->cancel();
            //1.2 将已有的会话重新加入到哈希表中
            _svr->set_timer(0, std::bind(&session_manager::append_already_session, this, sp, ssid));
            //2.将刚添加到哈希表中的会话关联上一个定时删除的任务
            wsserver_t::timer_ptr tmp = _svr->set_timer(ms, std::bind(&session_manager::destroy_session, this, ssid));
            //3.通过会话句柄sp，重新向会话中设置一个定时器，代表此会话为定时删除
            sp->set_timer(tmp);
        }

        //设置这么多session的状态有什么用呢？
        //1.在用户注册和登录的时候，都是http短链接，指定时间无通信后session应该被删除，不能永久存在。
        //2.当客户端与服务器建立websocket长连接之后，后续的所有客户端请求，服务器都是要通过ssid来验证用户是否登录的，只有登录的用户
        //  服务器才会为登录的用户提供服务，服务器不能一股脑的为所有用户(前面登录过，现在断开连接了)提供服务，这是不合理的！
        //  所以在建立连接之后，session必须永久存在，因为后续服务器处理每一个业务之前，都需要通过session模块来判断用户是否处于登录状态，
        //  判断用户依旧处于登录状态之后，才会为用户提供服务
    }
    void append_already_session(session_ptr sp, int ssid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _sessions.insert(std::make_pair(ssid, sp));// ---把已有的会话句柄重新放到哈希表里面---
    }

};



#endif